#!/usr/bin/env python3

import os
import re
import sys
from collections import defaultdict
from os.path import normpath


def main():
    name = None
    srcdirs = ['.']
    apps = []
    state = 'normal'
    mapping = defaultdict(list)
    previous_token = ''

    for line in open('Recipe').readlines():
        line = line.strip()
        if line.startswith('#') or not line:
            continue
        if 'block' == state:
            if '!end' == line:
                state = 'normal'
            continue
        if line.startswith('!'):
            parts = line.split(' ')
            if '!name' == parts[0]:
                assert 2 == len(parts)
                name = parts[1]
            elif '!makefile' == parts[0] or '!cflags' == parts[0]:
                pass  # ignored
            elif '!srcdir' == parts[0]:
                assert 2 == len(parts)
                srcdirs += [parts[1]]
            elif '!begin' == parts[0]:
                state = 'block'
            else:
                raise Exception("unknown token {}".format(parts[0]))
            continue

        parts = re.split(r"\s+", line)
        token = parts.pop(0)
        if '+' == token:
            token = previous_token
        else:
            op = parts.pop(0)
            if ':' == op:
                token += parts.pop(0)
                apps += [token]
            else:
                assert '=' == op
        assert parts
        mapping[token].extend(parts)
        previous_token = token

    # print(name)
    # print(srcdirs)
    # print(mapping)

    print("""\
# generated by mkcmake.py

# verbatim section:
cmake_minimum_required (VERSION 3.4)
project ({})

if (UNIX)
find_package (PkgConfig REQUIRED)
pkg_check_modules (GTK3 REQUIRED gtk+-3.0)

find_package (X11 REQUIRED)

include_directories (${{GTK3_INCLUDE_DIRS}} ${{X11_INCLUDE_DIR}})
link_directories (${{GTK3_LIBRARY_DIRS}})
add_definitions (${{GTK3_CFLAGS_OTHER}})

#configure_file (unix/uxconfig.h.in ${{CMAKE_CURRENT_BINARY_DIR}}/uxconfig.h)
#include_directories (${{CMAKE_CURRENT_BINARY_DIR}})
#add_definitions (-DHAVE_CONFIG_H)
endif (UNIX)

if (WIN32)
    add_definitions(-D_WINDOWS=1)
    add_definitions(-DNO_MANIFESTS=1)
endif (WIN32)

if (MSVC)
    # cmake's quoting rules are a bit mad here. An unquoted argument will become a list-type,
    # which will have semicolons randomly inserted when converted back to a string-type.
    # cflags can't be a list-type (???), so we must be careful to keep everything as a string
    
    # https://www.owasp.org/index.php/C-Based_Toolchain_Hardening#Visual_Studio
    set(EXTRA_FLAGS "${{EXTRA_FLAGS}} /GS")       # buffer security check
    set(EXTRA_FLAGS "${{EXTRA_FLAGS}} /guard:cf") # control flow guard

    set(EXTRA_FLAGS "${{EXTRA_FLAGS}} /wd4244")   # disable type conversion warnings :(((((
    set(EXTRA_FLAGS "${{EXTRA_FLAGS}} /wd4267")   # disable type conversion warnings :(((((

    add_definitions(-D_CRT_SECURE_NO_WARNINGS=1)

    set(CMAKE_C_FLAGS   "${{CMAKE_C_FLAGS}}   ${{EXTRA_FLAGS}}")
    set(CMAKE_CXX_FLAGS "${{CMAKE_CXX_FLAGS}} ${{EXTRA_FLAGS}}")

    set(CMAKE_C_FLAGS_DEBUG   "${{CMAKE_C_FLAGS_DEBUG}}   /MTd")
    set(CMAKE_CXX_FLAGS_DEBUG "${{CMAKE_CXX_FLAGS_DEBUG}} /MTd")

    set(CMAKE_C_FLAGS_RELEASE   "${{CMAKE_C_FLAGS_RELEASE}}   /MT /GL")
    set(CMAKE_CXX_FLAGS_RELEASE "${{CMAKE_CXX_FLAGS_RELEASE}} /MT /GL")

    set(CMAKE_EXE_LINKER_FLAGS_DEBUG   "${{CMAKE_EXE_LINKER_FLAGS_DEBUG}}   /guard:cf")
    set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${{CMAKE_EXE_LINKER_FLAGS_RELEASE}} /guard:cf /ltcg /opt:ref")
endif (MSVC)

if (MINGW)
    # undefined reference to `IN6_IS_ADDR_LOOPBACK'; probably a toolchain bug
    add_definitions(-DNO_IPV6=1)
    set(CMAKE_EXE_LINKER_FLAGS -static)
endif (MINGW)

# generated from Recipe:
""".format(name))

    print('include_directories ({})'.format(' '.join(
        sorted('${PROJECT_SOURCE_DIR}/' + normypath(src)
               for src in srcdirs))))

    for app in sorted(apps):
        name, platform = app.split('[')
        platform = platform[:-1]
        win = None
        if 'T' in platform:
            continue
        if platform in ['G', 'C']:
            win = True
        elif platform in ['X', 'U']:
            win = False
        else:
            raise Exception('bad platform: ' + platform)

        if win:
            print('if (WIN32)')
        else:
            print('if (UNIX)')

        expanded = list(expand(mapping, mapping[app]))
        files = sorted(to_path(srcdirs, expanded))

        # add headers, to placate CLion. Not needed by cmake.
        HEADER = re.compile(r'#\s*include\s+"([^"]+)"')
        headers = set()
        for file in files:
            with open(file) as f:
                for line in f.readlines():
                    ma = HEADER.match(line.strip())
                    if ma:
                        headers.add(ma.group(1))

        headers = sorted(find_literal_file(srcdirs, header)
                         for header in headers
                         # generated or build system nonsense
                         if header not in ['uxconfig.h', 'empty.h']
                         # evading #include "enum.c" (sigh)
                         and header.endswith('.h'))

        if win:
            headers.append('windows/winstuff.h')
            files.extend('windows/{}.rc'.format(x.split('.')[0]) for x in expanded if x.endswith('.res'))
        else:
            headers.append('unix/unix.h')

        print('add_executable({} {}\n  {}\n  {})'.format(
            name,
            'WIN32 windows/putty.manifest' if 'G' == platform else '# console app',
            ' '.join(files),
            ' '.join(sorted(headers)),
        ))

        if win:
            print('target_link_libraries ({}\n   {})'.format(
                name,
                ' '.join(file.split('.')[0] for file in expanded if file.endswith('.lib'))))
            print('if (MINGW)')
            print('target_link_libraries ({} -static-libgcc -static-libstdc++)'.format(name))
            print('endif (MINGW)')
            print('endif (WIN32)')
        else:
            print('target_link_libraries ({} -ldl)'.format(name))
            if 'X' == platform:
                print('target_link_libraries ({} '.format(name) +
                      '${GTK3_LIBRARIES} ${X11_LIBRARIES})')
            print('endif (UNIX)')


def expand(map, list):
    for item in list:
        if item in map:
            yield from expand(map, map[item])
        else:
            yield item


def to_path(dirs, items):
    for item in items:
        if '.' in item:
            if not item.endswith('.lib') and not item.endswith('.res'):
                sys.stderr.write('ignoring path {}\n'.format(item))
        else:
            yield find_c_file(dirs, item)


def find_c_file(dirs, item):
    for ext in ['c', 'cpp']:
        cand = dir_expand(dirs, '{}.{}'.format(item, ext))
        if cand:
            return cand

    raise Exception('no file named {}.[c|cpp] found in {}'
                    .format(item, dirs))


def find_literal_file(dirs, item):
    cand = dir_expand(dirs, item)
    if cand:
        return cand

    raise Exception('no file named {} found in {}'
                    .format(item, dirs))


def dir_expand(dirs, path):
    for sub in dirs:
        cand = '{}/{}'.format(sub, path)
        if os.path.isfile(cand):
            return normypath(cand)
    return None


def normypath(path):
    return normpath(path).replace('\\', '/')


if '__main__' == __name__:
    main()
